/************************************************
 *  @file cgi.cpp
 *  @brief Realization of client side of cgi iterface.
 * 	@see draft-coar-cgi-v11-03.txt
 ************************************************/
#include <cerrno>
#include <cstring>
#include <stdexcept>

#include "cgi.h"
#include "exception.h"
#include "ByteString.h"
#include "util.h"

//////////////////////////////////
// Constants
//////////////////////////////////
char * const mime_text_plain = "text/plain";
char * const mime_text_html  = "text/html";
char * const mime_text_css   = "text/css";
char * const mime_image_ico  = "image/ico"; 

namespace cgi_request {
		/// Flag which denie double initialization.
		static bool singleton;
		
		/// Auxiliary array for fast search of request params.
		static const struct RequestParam_t** request_param;

		/// Number of request params.
		static unsigned int request_param_count;

		/// Maximal length of incoming message.
		static size_t max_content_length;

		/// Content-length
		size_t content_length;

		/// Content-type
		const char * content_type;

		/// Gateway-interface
		const char * gateway_interface;

		/// Path-info
		const char * path_info;

		/// Query-string
		const char * query_string;

		/// Remote-address
		const char * remote_addr;

		/// Request-method
		const char * request_method;

		/// Script-name
		const char * script_name;

		/// Server-name
		const char * server_name;

		/// Server-port
		const char * server_port;

		/// Server-protocol
		const char * server_protocol;

		/// Server-software
		const char * server_software;

		/// Incoming message
		char * message_body;

		const struct RequestParam_t* request_param_list;

		////////////////////////////////////
		// Accessors.
		////////////////////////////////////

		size_t get_content_length() throw() {return content_length;}
		const char * get_content_type() throw() {return content_type;}
		const char * get_gateway_interface() throw() {return gateway_interface;}
		const char * get_path_info() throw() {return path_info;}
		const char * get_query_string() throw() {return query_string;}
		const char * get_remote_addr() throw() {return remote_addr;}
		const char * get_request_method() throw() {return request_method;}
		const char * get_script_name() throw() {return script_name;}
		const char * get_server_name() throw() {return server_name;}
		const char * get_server_port() throw() {return server_port;}
		const char * get_server_protocol() throw() {return server_protocol;}
		const char * get_server_software() throw() {return server_software;}
		const char * get_message_body() throw() {return message_body;}


		////////////////////////////////////
		// Functions.
		////////////////////////////////////

		/// Comparator of two RequestParam_t's (need for qsort).
		static int request_param_cmp(const void * _rp1, const void * _rp2);

		const struct RequestParam_t * get_param(const char * _key, size_t _key_size) throw () {
				struct RequestParam_t x = {_key, _key_size};
				const struct RequestParam_t * x_ptr = &x;
				
				void * ptr = bsearch(&x_ptr, request_param, request_param_count, sizeof(struct RequestParam_t*), &request_param_cmp);
				
				if( NULL == ptr )
						return NULL;
				else 
						return * (const struct RequestParam_t **) ptr;
		}

		const struct RequestParam_t * get_param_string(const char * _key) throw() {
				return get_param(_key, strlen(_key));
		}
		
		/// @brief Scan envorioment veriables.
		/// @exception std::invalid_argument undefined or wrong value one of MUST meta-variables
		static void read_env() throw( std::invalid_argument );

		/// @brief Read message body from stdin.
		/// @exception std::bad_alloc returned by new
		/// @exception std::length_error there is too big incoming package
		static void read_stdin() throw( std::bad_alloc, std::length_error, std::io_error );

		/// @brief Build params list base on _data.
		/// @param _data information for parsing
		/// @param _size sizeof input data size
		/// @exception std::bad_alloc returned by new
		static void build_request_param_list(const char * _data, size_t _size) throw( std::bad_alloc );

		/// @brief Build auxiliary array which sorted for fast finding.
		/// @exception std::bad_alloc returned by new
		static void build_request_param() throw( std::bad_alloc );

		
		static void read_env() throw( std::invalid_argument ) {
				const char * content_length_string = getenv("CONTENT_LENGTH");
				if( content_length_string )
						if( strlen(content_length_string) ) {
								char * endptr;
								errno = 0;
								content_length = strtoul(content_length_string, &endptr, 10);
								if( '\0' != endptr[0] || ERANGE == errno )
										throw std::invalid_argument("cgi_request::read_stdin(): wrong value of CONTENT_LENGTH.");
						}

				content_type = getenv("CONTENT_TYPE");
				if( content_type && !strlen(content_type) )
						content_type = NULL;

				gateway_interface = getenv("GATEWAY_INTERFACE");
				if( !gateway_interface )
						throw std::invalid_argument("cgi_request::read_env(): undefined GATEWAY_INTERFACE.");
				
				path_info = getenv("PATH_INFO");
				if( path_info && !strlen(path_info) )
						path_info = NULL;
				
				query_string = getenv("QUERY_STRING");
				if( query_string && !strlen(query_string) )
						query_string = NULL;
				
				remote_addr = getenv("REMOTE_ADDR");
				if( !remote_addr )
						throw std::invalid_argument("cgi_request::read_env(): undefinded REMOTE_ADDR.");
				
				request_method = getenv("REQUEST_METHOD");
				if( !request_method )
						throw std::invalid_argument("cgi_request::read_env(): undefinded REQUEST_METHOD.");
				
				script_name = getenv("SCRIPT_NAME");
				if( !script_name )
						script_name = "";
				
				server_name = getenv("SERVER_NAME");
				if( !server_name )
						throw std::invalid_argument("cgi_request::read_env(): undefinded SERVER_NAME.");
				
				server_port = getenv("SERVER_PORT");
				if( !server_port )
						throw std::invalid_argument("cgi_request::read_env(): undefinded SERVER_PORT.");
				
				server_protocol = getenv("SERVER_PROTOCOL");
				if( !server_protocol )
						throw std::invalid_argument("cgi_request::read_env(): undefinded SERVER_PROTOCOL.");				
				
				server_software = getenv("SERVER_SOFTWARE");
				if( !server_software )
						throw std::invalid_argument("cgi_request::read_env(): undefinded SERVER_SOFTWARE.");
		}
		
		static void read_stdin() throw( std::bad_alloc, std::length_error, std::io_error ) {
				if( 0 == content_length )
						return;

				if( -1 == util_setbinmode(util_stdin) )
						throw std::io_error("cgi_request::read_stdin(): error occured during set binary mode for stdin.");

				errno = 0;
				if( max_content_length && content_length > max_content_length ) {
						destruct();
						throw std::length_error("cgi_request::read_stdin(): message body too large.");
				}
				
				if( 0 == content_length ) 
						return;
				
				try {
						message_body = new char[ content_length ];
				} catch(...) {
						destruct();
						throw;
				}
				
				if( content_length != (size_t)util_readn(util_stdin, message_body, content_length) ) {
						destruct();
						throw std::io_error("cgi_request::read_stdin(): error occured during reading.");
				}
		}
		
		static void build_request_param_list(const char * _data, size_t _size) throw( std::bad_alloc ) {
									// look up throw _data
				for(size_t i = 0; i < _size; ++i) {

						// skip left characters
						while( i < _size && ('=' == _data[i] || '&' == _data[i]) )
								++i;
						if( _size == i )
								break;

						// finding key
						size_t key_pos = i, key_size = 0;
						while( i < _size && '=' != _data[i] && '&' != _data[i] ) 
								++i;
						key_size = i - key_pos;
						
						// finding value if it's exists
						size_t val_pos = i + 1, val_size = 0;
						if( i < _size && '=' == _data[i] ) {
								++i;

								while( i < _size && '&' != _data[i] )
										++i;
								val_size = i - val_pos;
						}
							
						// alloc new structure
						char * k_v = NULL;
						struct RequestParam_t * rp = NULL;
						char * key_tmp = NULL, * val_tmp = NULL;

						try {
								// reserv temprory array
								k_v = new char[key_size + val_size];

								// make DEHTTP transform
								char *key = k_v;
								char *val = k_v + key_size;
								
								memcpy(key, _data + key_pos, key_size);
								memcpy(val, _data + val_pos, val_size);
								
								key_size = util_dehttpize(key, key_size);
								val_size = util_dehttpize(val, val_size);

								// alloc new structure
								rp = new struct RequestParam_t;

								// copy key
								key_tmp = new char[key_size];
								rp->key = key_tmp;
								rp->key_size = key_size;
								memcpy(key_tmp, key, key_size);

								// copy value
								if( 0 != val_size ) 
										val_tmp = new char[val_size];
								rp->value = val_tmp;
								rp->value_size = val_size;
								memcpy(val_tmp, val, val_size);

								// insert record to list
								rp->next = request_param_list;
								request_param_list = rp;
								
								// delete temprory array
								delete [] k_v;
						} catch(...) {
								
								delete [] k_v;
								delete rp;
								delete [] key_tmp;
								delete [] val_tmp;

								destruct();
								throw;
						}
				}
		}

		static int request_param_cmp(const void * _rp1, const void * _rp2) {
				const struct RequestParam_t * rp1 = * (const struct RequestParam_t**) _rp1;
				const struct RequestParam_t * rp2 = * (const struct RequestParam_t**) _rp2;
				
				if( rp1->key_size < rp2->key_size )
						return -1;
				else if( rp1->key_size > rp2->key_size )
						return 1;
				else
						return memcmp(rp1->key, rp2->key, rp1->key_size);
		}

		static void build_request_param() throw( std::bad_alloc ) {
				// count number of parametres
				const struct RequestParam_t * rp_ptr = request_param_list;
				for(request_param_count = 0; NULL != rp_ptr; ++request_param_count)
						rp_ptr = rp_ptr->next;
				
				// if there are not parametres -- nothibg to do
				if( 0 == request_param_count )
						return;
				
				// alloc space for auxiliary array
				struct RequestParam_t **rp;
				try {
						rp = new struct RequestParam_t*[ request_param_count ];
				} catch(...) {
						destruct();
						throw;
				}

				request_param = (const struct RequestParam_t **)rp;
				
				// fill array based on list
				rp_ptr = request_param_list;
				for(size_t i = 0; i < request_param_count; ++i) {
						rp[i] = (struct RequestParam_t*)rp_ptr;
						rp_ptr = rp_ptr->next;
				}

				// sort array
				qsort(rp, request_param_count, sizeof(struct RequestParam_t*), &request_param_cmp);
		}

		void construct(size_t _max_content_length) throw(std::invalid_argument, std::length_error, std::bad_alloc, std::io_error, std::logic_error) {
				// store max content length
				max_content_length = _max_content_length;

				// denie second initialization
				if( singleton )
						throw std::logic_error("cgi_request::constructor(): second initialisation called.");
				singleton = true;
				
				read_env();
				read_stdin();
				
				if( NULL != query_string )
						build_request_param_list(query_string, strlen(query_string));
				
				if( NULL != message_body )
						build_request_param_list(message_body, content_length);

				build_request_param();
		}

		void destruct() throw() {
				const struct RequestParam_t *rp_ptr;
				
				delete [] message_body;
				delete [] request_param;
				
				while( NULL != request_param_list ) {
						rp_ptr = request_param_list;
						request_param_list = rp_ptr->next;

						delete [] rp_ptr->key;
						delete [] rp_ptr->value;
						delete rp_ptr;
				}
		}
}

namespace cgi_responde {

		////////////////////////////////////
		// Constants.
		////////////////////////////////////
		
		char * const status_200_ok                       = "200 Ok";
		char * const status_201_created                  = "201 Created";
		char * const status_202_accepted                 = "202 Accepted";

		char * const status_301_moved_permanently        = "301 Moved Permanently";
		char * const status_302_found                    = "302 Found";
		char * const status_303_see_other                = "303 See Other";

		char * const status_400_bad_request              = "400 Bad Request";
		char * const status_403_forbidden                = "403 Forbidden";
		char * const status_404_not_found                = "404 Not Found";
		char * const status_413_request_entity_too_large = "413 Request Entity Too Large";
		char * const status_415_unsupported_media_type   = "415 Unsupported Media Type";

		char * const status_500_internal_server_error    = "500 Internal Server Error";
		char * const status_501_not_implemented          = "501 Not implemented";
		char * const status_503_service_unavailable      = "503 Service Unavailable";

		////////////////////////////////////
		// Additional constants.
		////////////////////////////////////
		static const char   field_name_content_type[] = "Content-Type: ";
		static const size_t field_name_content_type_len = (sizeof(field_name_content_type) - 1);

		static const char   field_name_location[]     = "Location: ";
		static const size_t field_name_location_len   = (sizeof(field_name_location) - 1);
		
		static const char   field_name_status[]       = "Status: ";
		static const size_t field_name_status_len     = (sizeof(field_name_status) - 1);

		static const char   nl[]                      = "\r\n";
		static const size_t nl_len                    = (sizeof(nl) - 1);

		////////////////////////////////////
		// Variables.
		////////////////////////////////////

		/// Flag which denie double initialization.
		static bool singleton1;

		/// Flag which denie double send message.
		bool singleton2;

		/// Content-type field value.
		const char * content_type;
		
		/// Location field value.
		const char * location;
		
		/// Status field value.
		const char * status;
		
		/// Message content.
		static ByteString_t message;

		////////////////////////////////////
		// Accessors.
		////////////////////////////////////

		const char * get_content_type() throw() {
				return content_type;
		}

		void set_content_type(const char * _content_type) throw(std::bad_alloc) {
				char * buf = NULL;

				if( _content_type ) {
						size_t buf_size = strlen(_content_type) + 1;
						buf = new char[ buf_size ];
						memcpy(buf, _content_type, buf_size);
				}

				delete [] content_type;
				content_type = buf;
		}

		const char * get_location() throw() {
				return location;
		}
		
		void set_location(const char * _location) throw(std::bad_alloc) {
				char * buf = NULL;

				if( _location ) {
						size_t buf_size = strlen(_location) + 1;
						buf = new char[ buf_size ];
						memcpy(buf, _location, buf_size);
				}

				delete [] location;
				location = buf;
		}

		const char * get_status() throw() {
				return status;
		}

		void set_status(const char *_status) throw(std::bad_alloc) {
				char * buf = NULL;

				if( _status ) {
						size_t buf_size = strlen(_status) + 1;
						buf = new char[ buf_size ];
						memcpy(buf, _status, buf_size);
				}

				delete [] status;
				status = buf;
		}
		
		////////////////////////////////////
		// Functions.
		////////////////////////////////////

		void construct() throw( std::logic_error ) {
				if( singleton1 )
						throw std::logic_error("cgi_responde::constructor(): second initialisation called.");

				singleton1 = true;
		}
		
		void destruct() throw() {
				delete [] content_type;
				delete [] location;
				delete [] status;
		}

		void send() throw( std::bad_alloc, std::io_error, std::logic_error ) {
				// check unique flag
				if( singleton2 )
						throw std::logic_error("cgi_responde::send(): second initialisation called.");

				singleton2 = true;

				if( -1 == util_setbinmode(util_stdout) )
						throw std::io_error("cgi_responde::send(): error occured during set binary mode for stdout.");

				// set dafault status, if needed
				if( NULL == status )
						set_status(status_200_ok);
				
				// calc needed temprory space
				const size_t 
						status_len = strlen(status),
						location_len = (location ? strlen(location) : 0),
						content_type_len = (content_type ? strlen(content_type) : 0);

				const size_t 
						field_status = field_name_status_len + status_len + nl_len,
						field_location = (location_len ? field_name_location_len + location_len + nl_len : 0),
						field_content_type = (content_type_len ? field_name_content_type_len + content_type_len + nl_len : 0);
				
				const size_t buf_size = field_status + field_location + field_content_type + nl_len;
				
				// alloc space
				char * buf = new char[ buf_size ];

				// pointer
				char * ptr = buf;

				// status field
				memcpy(ptr, field_name_status, field_name_status_len);
				ptr += field_name_status_len;
				memcpy(ptr, status, status_len);
				ptr += status_len;
				memcpy(ptr, nl, nl_len);
				ptr += nl_len;

				// location field
				if( field_location ) {
						memcpy(ptr, field_name_location, field_name_location_len);
						ptr += field_name_location_len;
						memcpy(ptr, location, location_len);
						ptr += location_len;
						memcpy(ptr, nl, nl_len);
						ptr += nl_len;
				}

				// content-type field
				if( field_content_type ) {
						memcpy(ptr, field_name_content_type, field_name_content_type_len);
						ptr += field_name_content_type_len;
						memcpy(ptr, content_type, content_type_len);
						ptr += content_type_len;
						memcpy(ptr, nl, nl_len);
						ptr += nl_len;
				}

				// addition newline
				memcpy(ptr, nl, nl_len);
				ptr += nl_len;

				// output header
				if( -1 == util_writen(util_stdout, buf, buf_size) ) {
						delete [] buf;
						throw std::io_error("cgi_respond::send(): error occured during writing.");
				}

				// release temprory space
				delete [] buf;

				// send message body
				const size_t content_length = message.length();
				const char * message_body = message.array();

				if( content_length != (size_t)util_writen(util_stdout, message_body, content_length) )
						throw std::io_error("cgi_respond::send(): error occured during writing.");
		}

		void send_extra(const char* _status) throw() {
				const size_t status_len = strlen(_status);
				
				util_setbinmode(util_stdout);

				util_writen(util_stdout, field_name_status, field_name_status_len);
				util_writen(util_stdout, _status, status_len);
				util_writen(util_stdout, nl, nl_len);

				util_writen(util_stdout, field_name_content_type, field_name_content_type_len);
				util_writen(util_stdout, mime_text_plain, strlen(mime_text_plain));
				util_writen(util_stdout, nl, nl_len);

				util_writen(util_stdout, nl, nl_len);

				util_writen(util_stdout, _status, status_len);
		}

		////////////////////////////////////
		// Message work functions.
		////////////////////////////////////

		void append(const ByteString_t & _pak) throw( std::bad_alloc, std::out_of_range ) {
				message.append(_pak);
		}

		void append(const char * _array, size_t _n) throw( std::bad_alloc, std::out_of_range ) {
				message.append(_array, _n);
		}

		void append(const char * _cstr) throw( std::bad_alloc, std::out_of_range ) {
				message.append(_cstr);
		}

		void append(const char _c) throw( std::bad_alloc, std::out_of_range ) {
				message.append(_c);
		}

		void reserv(size_t _size)throw( std::bad_alloc, std::out_of_range ) {
				message.reserv(_size);
		}

		void clear() throw() {
				message.clear();
		}

}
